package org.skycloud.mybatis.filter;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectionException;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.session.RowBounds;
import org.springframework.util.StringUtils;

public final class FilterBuilder {

	private static final Object[] NO_ARGUMENTS = new Object[0];
	private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\p{javaWhitespace}+");

	private static final String SORT_KEYS = "_sortKeys";
	private static final String ASC = "ASC";
	private static final String DESC = "DESC";

	private final Map<String, Object> map = new HashMap<String, Object>();
	private final Map<String, Boolean> sortKeys = new LinkedHashMap<String, Boolean>();

	private int offset = RowBounds.NO_ROW_OFFSET;
	private int limit = RowBounds.NO_ROW_LIMIT;
	private boolean autoCount = false;

	private FilterBuilder(Object object) {
		add(object);
	}

	public static FilterBuilder newFilterBuilder() {
		return new FilterBuilder(null);
	}

	public static FilterBuilder newFilterBuilder(String propertyName, Object value) {
		return new FilterBuilder(null).add(propertyName, value);
	}

	public static FilterBuilder newFilterBuilder(Map<String, Object> map) {
		return new FilterBuilder(map);
	}

	public static FilterBuilder newFilterBuilder(Object object) {
		return new FilterBuilder(object);
	}

	public FilterBuilder add(String propertyName, Object value) {
		checkProperty(propertyName);
		map.put(propertyName, value);
		return this;
	}

	public FilterBuilder add(Map<String, Object> map) {
		return add((Object) map);
	}

	@SuppressWarnings("unchecked")
	public FilterBuilder add(Object object) {
		if (object != null) {
			if (object instanceof Map) {
				for (Entry<Object, Object> entry : ((Map<Object, Object>) object).entrySet()) {
					Object key = entry.getKey();
					if (!(key instanceof String)) {
						throw new IllegalArgumentException("Map key [" + key + "] is not [java.lang.String] type.");
					}

					String propertyName = (String) key;
					checkProperty(propertyName);
					map.put(propertyName, entry.getValue());
				}
			} else if (object instanceof Iterable) {
				throw new IllegalArgumentException(
						"Can not add [java.lang.Iterable] object to FilterBuilder, you may add a map or java bean.");
			}

			Class<?> cls = object.getClass();
			if (cls.isArray()) {
				throw new IllegalArgumentException(
						"Can not add an array to FilterBuilder, you may add a map or java bean.");
			}

			MetaClass metaClass = MetaClass.forClass(cls);
			for (String getterName : metaClass.getGetterNames()) {
				try {
					Invoker method = metaClass.getGetInvoker(getterName);
					try {
						map.put(getterName, method.invoke(object, NO_ARGUMENTS));
					} catch (Throwable t) {
						throw ExceptionUtil.unwrapThrowable(t);
					}
				} catch (RuntimeException e) {
					throw e;
				} catch (Throwable t) {
					throw new ReflectionException("Could not get property '" + getterName + "' from "
							+ object.getClass() + ".  Cause: " + t.toString(), t);
				}
			}
		}
		return this;
	}

	public FilterBuilder asc(String propertyName) {
		checkProperty(propertyName);
		if (WHITESPACE_PATTERN.matcher(propertyName).find()) {
			throw new IllegalArgumentException("Whitespace is not allowed for asc property [" + propertyName + "].");
		}

		sortKeys.put(propertyName, Boolean.TRUE);
		return this;
	}

	public FilterBuilder desc(String propertyName) {
		checkProperty(propertyName);
		if (WHITESPACE_PATTERN.matcher(propertyName).find()) {
			throw new IllegalArgumentException("Whitespace is not allowed for desc property [" + propertyName + "].");
		}

		sortKeys.put(propertyName, Boolean.FALSE);
		return this;
	}

	public FilterBuilder offset(int offset) {
		if (offset < 0) {
			throw new IllegalArgumentException("offset must not be less than 0.");
		}
		this.offset = offset;
		return this;
	}

	public FilterBuilder limit(int limit) {
		if (limit < 1) {
			throw new IllegalArgumentException("limit must not be less than 1.");
		}
		this.limit = limit;
		return this;
	}

	public FilterBuilder setAutoCount(boolean autoCount) {
		this.autoCount = autoCount;
		return this;
	}

	public Filter build() {
		if (!sortKeys.isEmpty()) {

			StringBuilder buf = new StringBuilder();
			int index = 0;

			for (Entry<String, Boolean> entry : sortKeys.entrySet()) {
				if (index > 0) {
					buf.append(',');
				}

				buf.append(entry.getKey()).append(' ');
				buf.append(entry.getValue() ? ASC : DESC);

				index++;
			}

			map.put(SORT_KEYS, buf.toString());
		}
		return new FilterImpl(map, offset, limit, autoCount);
	}

	private void checkProperty(String propertyName) {
		if (!StringUtils.hasText(propertyName)) {
			throw new IllegalArgumentException("propertyName must not be null or empty.");
		} else if (SORT_KEYS.equals(propertyName)) {
			throw new IllegalArgumentException("[" + SORT_KEYS
					+ "] is used for sorting purpose, you should not use this property directly.");
		}
	}

	private static class FilterImpl implements Filter {

		private final Map<String, Object> map;
		private final int offset;
		private final int limit;
		private final boolean autoCount;

		FilterImpl(Map<String, Object> map, int offset, int limit, boolean autoCount) {
			this.map = map;
			this.offset = offset;
			this.limit = limit;
			this.autoCount = autoCount;
		}

		@Override
		public Object get(String propertyName) {
			return map.get(propertyName);
		}

		@Override
		public Set<String> getPropertyNames() {
			return Collections.unmodifiableSet(map.keySet());
		}

		@Override
		public int getOffset() {
			return offset;
		}

		@Override
		public int getLimit() {
			return limit;
		}

		@Override
		public boolean isAutoCount() {
			return autoCount;
		}
	}
}
